package net.riaspace.flerry;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.Amf3Input;
import flex.messaging.io.amf.Amf3Output;
import flex.messaging.messages.AcknowledgeMessage;
import flex.messaging.messages.ErrorMessage;
import flex.messaging.messages.RemotingMessage;

public class NativeObject {

	public static final String STOP_PROCESS_HEADER = "STOP_PROCESS_HEADER";
	protected Class<?> sourceClass;
	protected Boolean singleton;
	protected Object singletonObject;

	protected static PrintStream out = System.out;
	protected static PrintStream err = System.err;
	protected static InputStream in = System.in;

	protected static Lock readWriteLock = new ReentrantLock();

	public NativeObject(Class<?> sourceClass, Boolean singleton) {
		try {
			System.setOut(new PrintStream(new FileOutputStream("out.log", true)));
			System.setErr(new PrintStream(new FileOutputStream("err.log", true)));
		} catch (FileNotFoundException e) {
			// TODO what to do here?
			try {
				System.setOut(new PrintStream(new FileOutputStream(
						"/tmp/flerry-out.log", true)));
				System.setErr(new PrintStream(new FileOutputStream(
						"/tmp/flerry-err.log", true)));
			} catch (FileNotFoundException e1) {
				e1.printStackTrace();
			}
		}

		this.sourceClass = sourceClass;
		this.singleton = singleton;
	}

	public void init() {
		final int BUFFER_SIZE = 512;
		char[] buffer = new char[BUFFER_SIZE];
		BufferedReader bufferedReader = new BufferedReader(
				new InputStreamReader(in));
		StringBuffer stringBuffer = new StringBuffer();

		try {
			int read;
			while ((read = bufferedReader.read(buffer, 0, buffer.length)) > 0) {
				stringBuffer.append(buffer, 0, read);
				String tempString = stringBuffer.toString();

				// if the string contains 2 markers
				if ((tempString.startsWith("_"))
						&& (tempString.indexOf("_", 1) != -1)) {
					// take string between the markers only
					int beginIndex = 1;
					int endIndex = tempString.indexOf("_", 1);
					String amfString = tempString.substring(beginIndex,
							endIndex);

					// decode Base64 string to AMF bytes
					byte[] amfBytes = Base64.decode(amfString);

					// create input stream for Amf3Input
					InputStream bais = new ByteArrayInputStream(amfBytes);
					Amf3Input amf3Input = new Amf3Input(
							SerializationContext.getSerializationContext());
					amf3Input.setInputStream(bais);

					// cleanup
					stringBuffer = new StringBuffer();
					tempString = "";

					try {
						Object object = amf3Input.readObject();
						if (object instanceof RemotingMessage) {
							RemotingMessage message = (RemotingMessage) object;
							try {
								if (message.getSource() != null) {
									Object sourceObject = null;
									if (singleton)
										sourceObject = singletonObject;

									if (sourceObject == null) {
										sourceObject = sourceClass
												.newInstance();
										if (singleton)
											singletonObject = sourceObject;
									}

									Object[] params = (Object[]) message
											.getBody();
									Class<?>[] paramsTypes = new Class[params.length];
									for (int i = 0; i < paramsTypes.length; i++) {
										if (params[i] == null)
											paramsTypes[i] = Object.class;
										else
											paramsTypes[i] = params[i]
													.getClass();
									}
									Object result = null;
									boolean resultDefined = false;
									NoSuchMethodException nsme = null;
									String methodName = message.getOperation();
									try {
										result = sourceObject
												.getClass()
												.getMethod(methodName,
														paramsTypes)
												.invoke(sourceObject, params);
										resultDefined = true;
									} catch (NoSuchMethodException e) {
										// error casting paramTypes, usually
										// caused by 'null' object...
										// try all methods matching name until
										// get result... or fail
										nsme = e;

										Method[] methods = sourceObject
												.getClass()
												.getDeclaredMethods();
										for (int i = 0; i < methods.length; i++) {
											Method method = methods[i];
											if (!methodName.equals(method
													.getName()))
												continue;

											try {
												result = method.invoke(
														sourceObject, params);
												resultDefined = true;
											} catch (IllegalAccessException e2) {
												continue;
											} catch (IllegalArgumentException e2) {
												continue;
											} catch (InvocationTargetException e2) {
												resultDefined = true;
												throw e2;
											}
										}
									}

									if (!resultDefined && nsme != null)
										throw nsme;
									sendMessage(result, message.getMessageId());

								} else {
									Boolean stopProcess = (Boolean) message
											.getHeader(NativeObject.STOP_PROCESS_HEADER);
									if (stopProcess != null && stopProcess) {
										break;
									}
								}
							}catch(InvocationTargetException e){
								handleException(e.getCause(), message.getMessageId());
							} catch (Exception e) {
								handleException(e, message.getMessageId());
							}
						} else {
							handleException(
									new Exception(
											"Received object is not RemotingMessage type!"),
									null);
						}
					} catch (ClassNotFoundException e) {
						e.printStackTrace();
					}
				}
			}
		} catch (IOException e) {
			handleException(e, null);
		}
	}

	protected static void handleException(Throwable e, String correlationId) {
		try {
			Amf3Output amf3Output = new Amf3Output(
					SerializationContext.getSerializationContext());
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			amf3Output.setOutputStream(baos);

			ErrorMessage message = new ErrorMessage();
			message.faultString = e.getMessage();
			message.faultDetail = e.toString();
			message.setCorrelationId(correlationId);

			amf3Output.writeObject(message);

			// Convert AMF binary data to Base64 string
			String amfString = Base64.encodeBytes(baos.toByteArray());

			// add markers at the beginning and the end of the string
			amfString = "_" + amfString + "_";

			err.write(amfString.getBytes("utf-8"));

			amf3Output.close();
		} catch (Exception e1) {
			e1.printStackTrace();
		}
	}

	public static void sendMessage(Object object, String correlationId) {
		try {

			Amf3Output amf3Output = new Amf3Output(
					SerializationContext.getSerializationContext());
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			amf3Output.setOutputStream(baos);

			AcknowledgeMessage message = new AcknowledgeMessage();
			message.setBody(object);
			message.setCorrelationId(correlationId);
			amf3Output.writeObject(message);

			// Convert AMF binary data to Base64 string
			String amfString = Base64.encodeBytes(baos.toByteArray());

			// add markers at the beginning and the end of the string
			amfString = "_" + amfString + "_";
			out.write(amfString.getBytes("utf-8"));

			amf3Output.flush();
			amf3Output.reset();
			amf3Output.close();
			// Thread.sleep(10);
		} catch (Exception e) {
			handleException(e, correlationId);
		}
	}

	public static void main(String[] args) {
		NativeObject.readWriteLock.lock();

		String source = null;
		Boolean singleton = false;
		for (int i = 0; i < args.length; i++) {
			if (args[i].equals("-source"))
				source = args[i + 1];
			else if (args[i].equals("-singleton"))
				singleton = Boolean.parseBoolean(args[i + 1]);
		}

		NativeObject nativeObject = null;
		try {
			nativeObject = new NativeObject(Class.forName(source), singleton);
			nativeObject.init();
		} catch (Exception e) {
			NativeObject.handleException(e, null);
		}

		NativeObject.readWriteLock.unlock();
	}
}